package com.gmcloud.auth.endpoint;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gmcloud.auth.support.handler.GmAuthenticationFailureEventHandler;
import com.gmcloud.common.core.constant.CacheConstants;
import com.gmcloud.common.utils.constant.CommonConstants;
import com.gmcloud.common.core.constant.SecurityConstants;
import com.gmcloud.common.core.utils.RedisUtil;
import com.gmcloud.common.utils.R;
import com.gmcloud.common.core.utils.RetOps;
import com.gmcloud.common.core.utils.SpringContextHolder;
import com.gmcloud.common.security.annotation.Inner;
import com.gmcloud.common.security.exception.OAuth2ErrorCodesExpand;
import com.gmcloud.common.security.exception.OAuthClientException;
import com.gmcloud.common.security.util.OAuth2EndpointUtils;
import com.gmcloud.upms.api.system.entity.SysOauthClientDetails;
import com.gmcloud.upms.api.system.feign.RemoteClientDetailsService;
import com.gmcloud.upms.api.system.entity.vo.TokenVo;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.cache.CacheManager;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.event.LogoutSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author zl.sir
 * @version 1.0
 * @since 2022/8/18 16:02  token端点
 */

@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
@Tag(name = "统一认证中心")
@RestController
@RequestMapping("/token")
public class GmTokenEndpoint {

    private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();

    private final AuthenticationFailureHandler authenticationFailureHandler = new GmAuthenticationFailureEventHandler();

    private final OAuth2AuthorizationService authorizationService;

    private final RemoteClientDetailsService clientDetailsService;

    private final RedisUtil redisUtil;

    private final CacheManager cacheManager;

    public GmTokenEndpoint(OAuth2AuthorizationService authorizationService, RemoteClientDetailsService clientDetailsService, RedisUtil redisUtil, CacheManager cacheManager) {
        this.authorizationService = authorizationService;
        this.clientDetailsService = clientDetailsService;
        this.redisUtil = redisUtil;
        this.cacheManager = cacheManager;
    }

    /**
     * 认证页面
     *
     * @param error 表单登录失败处理回调的错误信息
     * @return ModelAndView
     */
    @GetMapping("/login")
    public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
        modelAndView.setViewName("ftl/login");
        modelAndView.addObject("error", error);
        return modelAndView;
    }


    @GetMapping("/confirm_access")
    public ModelAndView confirm(Principal principal, ModelAndView modelAndView,
                                @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
                                @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
                                @RequestParam(OAuth2ParameterNames.STATE) String state) {
        SysOauthClientDetails clientDetails = RetOps
                .of(clientDetailsService.getClientDetailsById(clientId, SecurityConstants.FROM_IN)).getData()
                .orElseThrow(() -> new OAuthClientException("clientId 不合法"));

        Set<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
        modelAndView.addObject("clientId", clientId);
        modelAndView.addObject("state", state);
        modelAndView.addObject("scopeList", authorizedScopes);
        modelAndView.addObject("principalName", principal.getName());
        modelAndView.setViewName("ftl/confirm");
        return modelAndView;
    }

    /**
     * 退出并删除token
     *
     * @param authHeader Authorization
     */
    @DeleteMapping("/logout")
    public R<Boolean> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
        if (CharSequenceUtil.isBlank(authHeader)) {
            return R.ok();
        }

        String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), CharSequenceUtil.EMPTY).trim();
        return removeToken(tokenValue);
    }

    /**
     * 校验token
     *
     * @param token 令牌
     */
    @GetMapping("/check_token")
    public void checkToken(String token, HttpServletResponse response, HttpServletRequest request) throws ServletException, IOException {
        try (ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response)) {

            if (CharSequenceUtil.isBlank(token)) {
                httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
                this.authenticationFailureHandler.onAuthenticationFailure(request, response,
                        new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING));
            }
            OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);

            // 如果令牌不存在 返回401
            if (authorization == null || authorization.getAccessToken() == null) {
                this.authenticationFailureHandler.onAuthenticationFailure(request, response,
                        new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN));
            }

            assert authorization != null;
            Map<String, Object> claims = authorization.getAccessToken().getClaims();
            OAuth2AccessTokenResponse sendAccessTokenResponse = OAuth2EndpointUtils.sendAccessTokenResponse(authorization,
                    claims);
            this.accessTokenHttpResponseConverter.write(sendAccessTokenResponse, MediaType.APPLICATION_JSON, httpResponse);
        }
    }

    /**
     * 令牌管理调用
     *
     * @param token token
     */
    @Inner
    @DeleteMapping("/{token}")
    public R<Boolean> removeToken(@PathVariable("token") String token) {
        OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
        if (authorization == null) {
            return R.ok();
        }

        OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
        if (accessToken == null || CharSequenceUtil.isBlank(accessToken.getToken().getTokenValue())) {
            return R.ok();
        }
        // 清空用户信息
        Objects.requireNonNull(cacheManager.getCache(CacheConstants.USER_DETAILS)).evict(authorization.getPrincipalName());
        // 清空access token
        authorizationService.remove(authorization);
        // 处理自定义退出事件，保存相关日志
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        SpringContextHolder.publishEvent(new LogoutSuccessEvent(authentication));
        return R.ok();
    }

    /**
     * 查询token
     *
     * @param params 分页参数
     */
    @Inner
    @PostMapping("/page")
    public R<Page> tokenList(@RequestBody Map<String, Object> params) {
        // 根据分页参数获取对应数据
        String key = String.format("%s::*", CacheConstants.PROJECT_OAUTH_ACCESS);
        int current = MapUtil.getInt(params, CommonConstants.CURRENT);
        int size = MapUtil.getInt(params, CommonConstants.SIZE);
        Set<String> keys = redisUtil.getRedisTemplate().keys(key);
        assert keys != null;
        List<String> pages = keys.stream().skip((long) (current - 1) * size).limit(size).toList();
        Page result = new Page(current, size);

        List<TokenVo> tokenVoList = redisUtil.multiGet(pages).stream().map(obj -> {
            OAuth2Authorization authorization = (OAuth2Authorization) obj;
            TokenVo tokenVo = new TokenVo();
            tokenVo.setClientId(authorization.getRegisteredClientId());
            tokenVo.setId(authorization.getId());
            tokenVo.setUsername(authorization.getPrincipalName());
            OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
            tokenVo.setAccessToken(accessToken.getToken().getTokenValue());
            tokenVo.setExpiresAt(accessToken.getToken().getExpiresAt());
            tokenVo.setIssuedAt(accessToken.getToken().getIssuedAt());
            return tokenVo;
        }).toList();
        result.setRecords(tokenVoList);
        result.setTotal(keys.size());
        return R.ok(result);
    }
}
